<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body></body>
  <script>
    // 原始数据
    const data = { foo: 1, bar: 2 };
    // 存储副作用函数 重新设计的桶
    const bucket = new WeakMap();
    // 存储副作用函数的变量
    let activeEffect;

    // effect 栈
    const effectStack = []; // 新增

    // 清除依赖函数
    function cleanup(effectFn) {
      // 遍历 effectFn.deps 数组
      for (let i = 0; i < effectFn.deps.length; i++) {
        // deps 是依赖集合
        const deps = effectFn.deps[i];
        // 将 effectFn 从依赖集合中移除
        deps.delete(effectFn);
      }
      // 每次执行副作用函数都要清除一遍，最后需要重置 effectFn.dep 数组
      effectFn.deps.length = 0;
    }

    // 注册副作用函数的函数
    const effect = (fn, options = {}) => {
      const effectFn = () => {
        // 调用 cleanup 函数完成清除工作
        cleanup(effectFn);
        // 当 effectFn 执行时，将其设置为当前激活的副作用函数
        activeEffect = effectFn;
        // 在调用副作用函数之前将当前副作用函数压入栈中
        effectStack.push(effectFn); // 新增
        const res = fn();
        // 在当前副作用函数执行完毕后，将当前副作用函数弹出栈，并把 activeEffect 还原为之前的值
        effectStack.pop(); // 新增
        activeEffect = effectStack[effectStack.length - 1];
        return res;
      };
      // 将 options 挂在到 effectFn 上
      effectFn.options = options; // 新增
      // activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
      effectFn.deps = [];
      if (!options.lazy) {
        // 执行副作用函数
        effectFn();
      }
      return effectFn;
    };

    // track 获取
    const track = (target, key) => {
      // 没有副作用函数直接返回
      if (!activeEffect) return;
      // 获取桶以targe作为键名，以set作为值
      let desMap = bucket.get(target);
      // 如果这个桶中之前没有收集这个对象的依赖那么就新建一个  Map
      if (!desMap) {
        bucket.set(target, (desMap = new Map()));
      }
      // 获取桶中对应对象的 desMap 中的 以键名作为key,以副作用函数集合作为值的数据
      let deps = desMap.get(key);
      // 如果该key为空，那么重新设置set集合
      if (!deps) {
        desMap.set(key, (deps = new Set()));
      }
      // 把当前激活的副作用函数添加到依赖集合 deps 中
      deps.add(activeEffect);
      // deps 就是一个与当前副作用函数存在联系的依赖集合
      // 将其添加到 activeEffect.deps 数组中
      activeEffect.deps.push(deps); // 新增
    };

    // trigger 设置
    const trigger = (target, key) => {
      // 根据响应对象获取 依赖集合
      const desMap = bucket.get(target);
      if (!desMap) return true;
      // 根据key获取对应属性的依赖集合
      const effects = desMap.get(key);
      // 避免无限循环
      const effectsToRun = new Set();
      effects &&
        effects.forEach((effectFn) => {
          // 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同，则不触发执行
          // 新增
          if (effectFn !== activeEffect) {
            effectsToRun.add(effectFn);
          }
        });
      effectsToRun.forEach((effectFn) => {
        // 如果一个副作用函数存在调度器，则调用该调度器，并将副作用函数作为参数传递
        if (effectFn.options.scheduler) {
          // 新增
          effectFn.options.scheduler(effectFn); // 新增
        } else {
          // 否则直接执行副作用函数
          effectFn();
        }
      });
    };

    const obj = new Proxy(data, {
      get(target, key) {
        track(target, key);
        return target[key];
      },
      set(target, key, newVal) {
        target[key] = newVal;
        trigger(target, key);
      }
    });

    // 递归读取对象的值
    function traverse(value, seen = new Set()) {
      // 如果读取的数据是原始值，或者已经被读取过了，那么什么都不做
      if (typeof value !== "object" || value === null || seen.has(value))
        return;
      // 将数据添加到 seen 中，代表遍历地读取过了，避免循环引起的死循环
      seen.add(value);
      // 暂时不考虑数据等其他结构
      // 假设 value 就是一个对象，使用 for...in 读取对象的每一个值，并递归调用 traverse 进行处理
      for (const k in value) {
        traverse(value[k], seen);
      }

      return value;
    }

    // watch 函数接收两个参数，source 是响应式数据，cb 是回调函数
    function watch(source, cb, options = {}) {
      // 定义 getter
      let getter;
      // 如果source 是函数，说明用户传递的是 getter，所以直接把 source 复制给 getter
      if (typeof source === "function") {
        getter = source;
      } else {
        // 否则按照原来的实现调用 traverse 函数
        getter = () => traverse(source);
      }
      // 定义旧值与新值
      let oldValue, newValue;

      // cleanup 用来存储用户注册的过期回调
      let cleanup;
      // 定义 onInvalidate 函数
      function onInvalidate(fn) {
        // 将过期回调存储到 cleanup 中
        cleanup = fn;
      }

      // 提取 scheduler 调度函数为一个独立的 job 函数
      const job = () => {
        // 在 scheduler 中重新执行副作用函数，得到的是新值
        newValue = effectFn();

        // 在调用回调函数 cb 之前，先调用过期回调
        if (cleanup) {
          cleanup();
        }

        // 将旧值和新值作为回调函数的参数
        cb(newValue, oldValue, onInvalidate);
        // 更新旧值，不然下一次会得到错误的旧值
        oldValue = newValue;
      };

      // 使用 effect 注册副作用函数时，开启lazy 选项，并把返回值存储 effectFn 中以便后续手动调用
      const effectFn = effect(
        // 执行 getter
        () => getter(),
        {
          lazy: true,
          scheduler: () => {
            // 在调度函数中判断 flush 是否为 'post'，如果是将其放到微任务队列中执行
            if (options.flush === "post") {
              const p = Promise.resolve();
              p.then(job);
            } else {
              job();
            }
          }
        }
      );
      if (options.immediate) {
        job();
      } else {
        // 手动调用副作用函数，拿到的值就是旧值
        oldValue = effectFn();
      }
    }

    watch(
      () => obj.foo,
      (newVal, oldValue) => {
        console.log("newVal, oldValue: ", newVal, oldValue);
        console.log("watch 数据发生变化了");
      },
      {
        immediate: true
      }
    );
    obj.foo++;
    setTimeout(() => {
      obj.foo++;
    }, 3000);
  </script>
</html>
